OASでLambda AuthorizerをREST APIにimportする
API Gateway REST APIを作成する場合にLambda AuthorizerでAPIを保護するのはよくあるケースです.
またAPI定義をOAS (Open API Specification) で記載するのもよくあるケースです.
API GayewayのドキュメントではOAS 拡張でどのようにLambda Authorizerを記載しているかは書いてあるのですが, 実装をどのようにすべきかがいまいちわからなかったので試してみました.
write lambda function
まずはREST APIから呼び出すLambda Authorizerのための関数と統合バックエンドのための関数を書いていきます.
どちらの関数に関してもパッケージの初期処理と必要な依存関係を入れるために下記の処理を事前に行います.
$ yarn init -y $ yarn add -D typescript @types/aws-lambda @types/node
Lambda Authorizer Function
まずはLambda Authorizerで利用する関数を書いていきます.
REST APIから流れてくるリクエストの種類としては大まかに2つに分けることができます. そして今回はトークンを利用してAPIを保護します.
Lambda Authorizerではないですが, Cognito User Poolを利用した保護も可能です.
- Token based Lambda Authorizer: JWTやOAuthトークンなどのベアラトークンを受け取ってアクセスを制限するパターン
- REQUEST based Lambda Authorizer: ヘッダーやクエリストリングなど経由でトークンを受け取ってアクセスを制御するパターン
REST APIからLambda Authorizerにトークンを利用する場合に渡される情報は下記のようになります.
typeはそのままどの方法かで, authorizationTokenがクライアントがAPIに対して渡したトークンになります.
なのでLambda AuthorizerではauthorizationTokenで渡されたトークンを検証することでAPIの保護を実現します.
最後にmethodArnですが, REST APIが実行したいメソッドのARNになります.
ここだけでは頭にはてなが100個くらい浮かびますが, Lambda Authorizer Functionの出力を見れば納得できますのでいったんはそのはてなをコインに変えてください.
{ "type":"TOKEN", "authorizationToken":"{caller-supplied-token}", "methodArn":"arn:aws:execute-api:{regionId}:{accountId}:{apiId}/{stage}/{httpVerb}/[{resource}/[{child-resources}]]" }
次にLambda AuthorizerがREST APIに対して返すべきレスポンスをみていきます.
レスポンスで最も重要なのは統合バックエンドのLambda関数呼び出しを許可または拒否するポリシードキュメントの部分です.
REST APIからのリクエストにmethodArnが含まれていたのはポリシードキュメント生成で必要になるためです.
principalIdはリクエストのクライアントを一意にし, contextの情報を統合バックエンドにcontextとして渡すために利用します.
ユーザ情報などcontextで渡すことで処理をしたりできますね.
usageIdentifierKeyは名前の通りAPI ステージの使用量プランのAPIキーを指します.
{ "principalId": "yyyyyyyy", "policyDocument": { "Version": "2012-10-17", "Statement": [ { "Action": "execute-api:Invoke", "Effect": "Allow|Deny", "Resource": "arn:aws:execute-api:{regionId}:{accountId}:{apiId}/{stage}/{httpVerb}/[{resource}/[{child-resources}]]" } ] }, "context": { "stringKey": "value", "numberKey": "1", "booleanKey": "true" }, "usageIdentifierKey": "{api-key}" }
実際にLambda Functionを実装していきます.
今回はクライアントからTokenが送られてきてかつ, 「super-super-secure-token」というとても強固で誰も見破ることができ無いであろうトークンだった場合にはバックエンドのLambda関数を呼び出します.
そうでない場合にはアクセスを拒否します.
import {APIGatewayAuthorizerEvent, APIGatewayAuthorizerResult} from 'aws-lambda' async function handler(event: APIGatewayAuthorizerEvent): Promise<APIGatewayAuthorizerResult> { if (event.type !== 'TOKEN') { return { principalId: null, policyDocument: { Version: '2012-10-17', Statement: [ { Action: 'execute-api:Invoke', Effect: 'Deny', Resource: event.methodArn, } ] }, } } console.log(`handle even\ntype: ${event.type}\ntoken: ${event.authorizationToken}`) if (event.authorizationToken === 'super-super-secure-token') { return { principalId: 'super-secure-boy', policyDocument: { Version: '2012-10-17', Statement: [ { Action: 'execute-api:Invoke', Effect: 'Allow', Resource: event.methodArn, } ] }, } } return { principalId: null, policyDocument: { Version: '2012-10-17', Statement: [ { Action: 'execute-api:Invoke', Effect: 'Deny', Resource: event.methodArn, } ] }, } } export { handler }
とても簡単な実装ではありますが, アクセスを拒否したい場合は「Deny」を持ったポリシーを, そうでない場合は「Allow」を持ったポリシーを返します.
最後に関数をデプロイします.
# build handler $ yarn run tsc index.ts $ zip index.zip index.js $ aws --region us-east-1 lambda create-function \ --function-name sdx_authorizer \ --runtime nodejs12.x \ --role arn:aws:iam::123456789012:role/lambda-role \ --handler index.handler \ --zip-file fileb://index.zip
Backend Lambda Function
バックエンドのLambda関数に関しては通常の統合バックエンドで利用するLambda関数を作成すれば問題ありません.
私たちがとてもセキュアなAPIを用意して守りたかった内容はバンドメンバーとパート一覧です. 間違いありませんよね?
私はそう思っています.
import {Context, APIGatewayProxyEvent, APIGatewayProxyResult} from 'aws-lambda' async function handler(event: APIGatewayProxyEvent, context: Context): Promise<APIGatewayProxyResult> { const functionName = context.functionName const version = context.functionVersion console.log(`function: ${functionName}\nversion: ${version}`) const method = event.httpMethod const body = event.body console.log(`method: ${method}\nbody: ${body}`) const members = { members: [ { id: 1, name: 'Anthony Kiedis', part: 'vocal', }, { id: 2, name: 'Chad Smith', part: 'drum' }, { id: 3, name: 'Flea', part: 'bass guitar', }, { id: 4, name: 'John Frusciante', part: 'guitar', }, ] } return { body: JSON.stringify(members), headers: { 'Content-Type': 'application/json' }, statusCode: 200, isBase64Encoded: false, } } export { handler }
特に解説することもないのでデプロイしていきます.
# build handler $ yarn run tsc index.ts $ zip index.zip index.js $ aws --region us-east-1 lambda create-function \ --function-name sdx_handler \ --runtime nodejs12.x \ --role arn:aws:iam::123456789012:role/lambda-role \ --handler index.handler \ --zip-file fileb://index.zip
これでLambda Functionsの準備は完了しました. 次にすべきはそうですね, OASを書いていきます.
Write OAS
つぎにOASを書いていきます.
基本的にはOAS通りに書いていき, securitySchemes部分にLambda Authorizerの設定を書いていきます.
今回はsecuritySchemesを利用するのでpathにsecurityを追加します.
今回はScopeでの絞り込みはしないため値には空の配列を渡します.
paths: /members: get: summary: lists all members security: - sdx_authorizer: []
ちょっとだけ飛ばして, componentsの中身をみていきます.
ここにLambda Authorizerで必要な情報を定義していきます. OASのSecuritySchemasで必要な項目に加えて, 「x-amazon-apigateway-authtype」と「x-amazon-apigateway-authorizer」を指定します.
今回はヘッダでtokenを受け取ってAPIの保護を行うので下記のように設定を行います.
components: securitySchemes: sdx_authorizer: type: apiKey description: Lambda Authorizer name: Authorization in: header x-amazon-apigateway-authtype: custom x-amazon-apigateway-authorizer: authorizerUri: arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:sdx_authorizer/invocations authorizerResultTtlInSeconds: 300 type: "token"
全体を通した定義は下記のようになります.
openapi: '3.0.2' info: title: super secure web API description: Lambda Authorizer Sample version: '1.0' servers: - url: "https://{id}.execute-api.us-east-1.amazonaws.com/{basePath}" variables: basePath: default: /v1 id: default: xxxxxxxxx paths: /members: get: summary: lists all members security: - sdx_authorizer: [] responses: 200: description: "200 response" content: application/json: schema: $ref: "#/components/schemas/Member" examples: jsonObject: summary: A Sample Member Object $ref: '#/components/examples/memberExample' x-amazon-apigateway-integration: type: aws_proxy httpMethod: POST passthroughBehavior: when_no_match uri: arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:sdx_basic/invocations responses: default: statusCode: '200' security: - sdx_authorizer: [] components: securitySchemes: sdx_authorizer: type: apiKey description: Lambda Authorizer name: Authorization in: header x-amazon-apigateway-authtype: custom x-amazon-apigateway-authorizer: authorizerUri: arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:sdx_authorizer/invocations authorizerResultTtlInSeconds: 300 type: "token" schemas: Empty: title: "Empty Schema" type: "object" Member: type: object properties: id: type: integer name: type: string part: type: string examples: memberExample: value: id: 1 name: dave navarro part: guitar
これでOASとLambda Functionsが揃いました.
あとはデプロイするだけです.
create API
REST APIを作成します.
$ aws --region us-east-1 apigateway import-rest-api \ --fail-on-warnings \ --body file://define.yaml { "id": "xxxxxxxxxx", "name": "super secure web API", "description": "Lambda Authorizer Sample", "createdDate": 1591338069, "version": "1.0", "apiKeySource": "HEADER", "endpointConfiguration": { "types": [ "EDGE" ] } }
次にLambdaのリソースポリシーでAPI GatewayからのLambda関数の発火を許可します.
$ aws --region us-east-1 lambda add-permission \ --function-name sdx_basic \ --action lambda:InvokeFunction \ --statement-id super_secure_backend \ --principal apigateway.amazonaws.com \ --source-arn 'arn:aws:execute-api:us-east-1:123456789012:xxxxxxxxxx/*/*/*' $ aws --region us-east-1 lambda add-permission \ --function-name sdx_authorizer \ --action lambda:InvokeFunction \ --statement-id super_secure_authorizer \ --principal apigateway.amazonaws.com \ --source-arn 'arn:aws:execute-api:us-east-1:123456789012:xxxxxxxxxx/authorizers/*'
最後にAPI Gatewayをデプロイします.
$ aws --region us-east-1 apigateway create-deployment \ --rest-api-id xxxxxxxxxx \ --stage-name v1 { "id": "xxxxx", "createdDate": 1591338284 } 今までの設定でとても強固に守られたAPIにアクセスしてみます. $ curl https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/v1/members {"message":"Unauthorized"} $ curl -H "Authorization: super-super-secure-token" https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/v1/members {"members":[{"id":1,"name":"Anthony Kiedis","part":"vocal"},{"id":2,"name":"Chad Smith","part":"drum"},{"id":3,"name":"Flea","part":"bass guitar"},{"id":4,"name":"John Frusciante","part":"guitar"}]}
とても強固に守られていますね.
To close
Lambda Authorizerは用語が多いことやコンポーネントが多いのでこんがらがりやすいですが,
- バックエンドの呼び出しを保護しかつ, ある程度保護とバックエンドの実行を疎にする
- Lambda AuthorizerはAPI Gateway経由でLambda 関数を呼び出して, ポリシードキュメントを受け取って評価する
このことを意識していれば大体は理解できると思います. その上でのインタフェースが今回記載したOASになります.
この記事がお役立ちましたら幸いです.